/* CheckSignalDisplay.java created 2010-10-24
 *
 */

package org.signalml.app.view.document.monitor.signalchecking;

import java.awt.BasicStroke;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Point;
import java.awt.geom.Rectangle2D;
import java.text.DecimalFormat;
import java.text.NumberFormat;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.signalml.app.view.montage.visualreference.VisualReferenceBin;
import org.signalml.app.view.montage.visualreference.VisualReferenceDisplay;
import org.signalml.app.view.montage.visualreference.VisualReferenceModel;
import org.signalml.app.view.montage.visualreference.VisualReferenceSourceChannel;

/**
 * A component which - based on information from a {@link VisualReferenceModel} object
 * and a {@link GenericAmplifierDiagnosis} object - draws the state of each channel.
 *
 * @author Tomasz Sawicki
 */
public class CheckSignalDisplay extends VisualReferenceDisplay {

	/**
	 * Space between two bars or bar and a channel.
	 */
	public static final int BAR_PADDING = 12;
	/**
	 * Width of a bar.
	 */
	public static final int BAR_WIDTH = 20;
	/**
	 * Height of a bar.
	 */
	public static final int BAR_HEIGHT = 50;
	/**
	 * Bar border size.
	 */
	public static final int BAR_BORDER = 2;
	/**
	 * Text space between text and bar.
	 */
	public static final int TEXT_SPACE = 3;
	/**
	 * How much the bar is moved vertically.
	 */
	public static final int BAR_VERTICAL_OFFSET = -14;
	/**
	 * Channel info used when drawing the channel state.
	 */
	private List<HashMap<String, ChannelState>> channels;

	/**
	 * Default construcot only calls super.
	 *
	 * @param model super parameter
	 */
	public CheckSignalDisplay(VisualReferenceModel model) {

		super(model);
	}

	/**
	 * Sets the given channels state, then repaints the component
	 * to visualize any possible changes.
	 *
	 * @param channels HashMap representing channels state
	 */
	public void setChannelsState(List<HashMap<String, ChannelState>> channels) {

		this.channels = channels;
		this.repaint();
	}

	/**
	 * Draws bin contents based on the channel state.
	 *
	 * @param bin {@link VisualReferenceBin} object
	 * @param g {@link Graphics2D} object
	 */
	@Override
	protected void paintBinContents(VisualReferenceBin bin, Graphics2D g) {

		HashMap<String, Boolean> validData = null;
		HashMap<String, List<AdditionalChannelData>> additionalData = null;

		boolean stateKnown = isStateKnown();
		if (stateKnown) {

			validData = getValidData();
			additionalData = getAdditionalData();
		}


		Iterator<VisualReferenceSourceChannel> it = bin.iterator();
		VisualReferenceSourceChannel channel;

		if (!stateKnown) {

			while (it.hasNext()) {

				channel = it.next();
				paintUnknownChannel(channel, g);
			}

		} else {

			while (it.hasNext()) {

				channel = it.next();
				String channelLabel = channel.getLabel();
				Boolean valid = validData.get(channelLabel);
				if (valid != null) {
					//ZERO and ONE channels are not in the validData
					//and should not be analyzed.
					List<AdditionalChannelData> data = additionalData.get(channel.getLabel());
					paintKnownChannel(channel, g, valid, data);
				}
			}
		}
	}

	/**
	 * Draws an unknown channel - white fill and red thick outline.
	 *
	 * @param channel {@link VisualReferenceSourceChannel} object
	 * @param g {@link Graphics2D} object
	 */
	private void paintUnknownChannel(VisualReferenceSourceChannel channel, Graphics2D g) {

		paintGivenChannel(channel.getLabel(), 0, channel.getShape(), channel.getOutlineShape(), Color.WHITE, Color.RED, true, g);
	}

	/**
	 * Draws a known channel - green fill and black thin outline if valid, red fill
	 * and black thick outline if invalid. Also draws the additional data.
	 *
	 * @param channel {@link VisualReferenceSourceChannel} object
	 * @param g {@link Graphics2D} object
	 * @param valid whether channel is valid
	 * @param additionalData list of additional data to draw
	 */
	private void paintKnownChannel(VisualReferenceSourceChannel channel, Graphics2D g, boolean valid, List<AdditionalChannelData> additionalData) {

		paintGivenChannel(channel.getLabel(), 0, channel.getShape(), channel.getOutlineShape(), (valid) ? Color.GREEN : Color.RED, Color.BLACK, false, g);

		if (!additionalData.isEmpty()) {

			int startX = channel.getLocation().x + VisualReferenceSourceChannel.CIRCLE_DIAMETER + BAR_PADDING;
			int startY = channel.getLocation().y + BAR_VERTICAL_OFFSET + (VisualReferenceSourceChannel.CIRCLE_DIAMETER - BAR_HEIGHT) / 2;
			Point location = new Point(startX, startY);
			for (AdditionalChannelData data : additionalData) {

				paintAdditionalDataBar(location, g, data);
				startX += BAR_WIDTH + BAR_PADDING;
				location = new Point(startX, startY);
			}

		}
	}

	/**
	 * Paints a value bar.
	 * @param location location of the bar
	 * @param g {@link Graphics2D} object
	 * @param data data to draw
	 */
	private void paintAdditionalDataBar(Point location, Graphics2D g, AdditionalChannelData data) {

		paintBar(location, g, data);
		paintLabels(location, g, data);
	}

	/**
	 * Paints a value bar.
	 * @param location location of the bar
	 * @param g {@link Graphics2D} object
	 * @param data data to draw
	 */
	private void paintBar(Point location, Graphics2D g, AdditionalChannelData data) {

		int validHeight = (int)(BAR_HEIGHT * ((data.getLimit() - data.getMin()) / (data.getMax() - data.getMin())));
		int validStart = BAR_HEIGHT - validHeight;
		int currentHeight = (int)((data.getCurrent() < data.getMax()) ? (BAR_HEIGHT * ((data.getCurrent() - data.getMin()) / (data.getMax() - data.getMin()))) : BAR_HEIGHT);
		int currentEnd = BAR_HEIGHT - currentHeight;

		g.setPaint(Color.RED);
		g.fillRect(location.x, location.y, BAR_WIDTH, BAR_HEIGHT - validHeight);

		g.setPaint(new GradientPaint(location.x, location.y + validStart, Color.RED, location.x, location.y + BAR_HEIGHT, Color.GREEN));
		g.fillRect(location.x, location.y + validStart, BAR_WIDTH, validHeight);

		g.setPaint(Color.WHITE);
		g.fillRect(location.x, location.y, BAR_WIDTH, currentEnd);

		g.setPaint(Color.BLACK);
		g.setStroke(new BasicStroke(BAR_BORDER));
		g.drawRect(location.x, location.y, BAR_WIDTH, BAR_HEIGHT);
		g.drawLine(location.x, location.y + validStart, location.x + BAR_WIDTH, location.y + validStart);


	}

	/**
	 * Paints bar labels: max, min, current, and method name
	 * @param location location of the bar
	 * @param g {@link Graphics2D} object
	 * @param data data to draw
	 */
	private void paintLabels(Point location, Graphics2D g, AdditionalChannelData data) {

		g.setFont(channelLabelFont);

		String toWrite = formatDouble(data.getMax());
		Rectangle2D rect = channelLabelFontMetrics.getStringBounds(toWrite, g);
		int textHeight = (int)rect.getHeight();
		int textWidth = (int)(rect.getWidth());
		int offset = (BAR_WIDTH - textWidth) / 2;
		int textX = location.x + offset;
		int textY = location.y - TEXT_SPACE;
		paintLabelWithBackground(toWrite, textX, textY, textWidth, textHeight, g);

		toWrite = formatDouble(data.getCurrent());
		rect = channelLabelFontMetrics.getStringBounds(toWrite, g);
		textHeight = (int)rect.getHeight();
		textWidth = (int)rect.getWidth();
		offset = (BAR_WIDTH - textWidth) / 2;
		textX = location.x + offset;
		textY = textY - textHeight;
		paintLabelWithBackground(toWrite, textX, textY, textWidth, textHeight, g);

		toWrite = formatDouble(data.getMin());
		rect = channelLabelFontMetrics.getStringBounds(toWrite, g);
		textHeight = (int)rect.getHeight();
		textWidth = (int)rect.getWidth();
		offset = (BAR_WIDTH - textWidth) / 2;
		textX = location.x + offset;
		textY = location.y + textHeight + BAR_HEIGHT + TEXT_SPACE;
		paintLabelWithBackground(toWrite, textX, textY, textWidth, textHeight, g);
	}

	/**
	 * Paints given String with white background.
	 * @param toWrite String to write
	 * @param textX x position
	 * @param textY y position
	 * @param textWidth text width
	 * @param textHeight text height
	 * @param g {@link Graphics2D} object
	 */
	private void paintLabelWithBackground(String toWrite, int textX, int textY, int textWidth, int textHeight, Graphics2D g) {

		g.setPaint(Color.WHITE);
		g.fillRect(textX, textY - textHeight, textWidth, textHeight);
		g.setPaint(Color.BLACK);
		g.drawString(toWrite, textX, textY);
	}

	/**
	 * Formats given double.
	 *
	 * @param input input value
	 * @return input as {@link String}
	 */
	private String formatDouble(double input) {

		NumberFormat formatter;
		if (input < 1e6)
			formatter = new DecimalFormat("00.00");
		else
			formatter = new DecimalFormat("0E00");

		return formatter.format(input);
	}

	/**
	 * Whether the state is known - all diagnosis objects returned non-null value.
	 *
	 * @return true if the state is known, false otherwise
	 */
	private boolean isStateKnown() {

		for (Object diagnosisResult : channels)
			if (diagnosisResult == null)
				return false;
		return true;
	}

	/**
	 * For each channel, checks if that channel is valid.
	 *
	 * @return valid data on each channel
	 */
	private HashMap<String, Boolean> getValidData() {

		HashMap<String, Boolean> validData = new HashMap<String, Boolean>();
		Set<String> channelNames = channels.get(0).keySet();

		for (String channel : channelNames) {

			boolean valid = true;
			for (HashMap<String, ChannelState> diagnosisResult : channels) {

				if (!diagnosisResult.get(channel).isValid()) {

					valid = false;
					break;
				}
			}

			validData.put(channel, valid);
		}

		return validData;
	}

	/**
	 * Gets list additional channel data for each channel.
	 *
	 * @return additional data
	 */
	private HashMap<String, List<AdditionalChannelData>> getAdditionalData() {

		HashMap<String, List<AdditionalChannelData>> additionalData =
			new HashMap<String, List<AdditionalChannelData>>();

		Set<String> channelNames = channels.get(0).keySet();

		for (String channel : channelNames) {

			List<AdditionalChannelData> channelsAdditionalData = new ArrayList<AdditionalChannelData>();
			for (HashMap<String, ChannelState> diagnosisResult : channels) {

				AdditionalChannelData currentData = diagnosisResult.get(channel).getAdditionalChannelData();
				if (currentData != null)
					channelsAdditionalData.add(currentData);
			}

			additionalData.put(channel, channelsAdditionalData);
		}

		return additionalData;
	}

}
